package oop.ex2.tokenizer;

import java.util.ArrayList;

import oop.ex2.exceptions.TokenNotRecognized;
import oop.ex2.expression.ExpressionNode;
import oop.ex2.expression.FunctionNode;
import oop.ex2.expression.NodeType;

/**
 * This takes an expression as input and lets the user advance from token to token. A token is
 * any sequence of symbols with a specific meaning such as a function name, operator, parenthesis etc...
 * In addition, at any given state it can return an ExpressionNode corresponding to the current token.
 * 
 * It advances through the expression by cutting off the parts of the expression that it finds and
 * storing them as ExpressionNodes.
 * @author daniel_i_l
 *
 */

public class Tokenizer {
	private String _expression;
	private ExpressionNode _currentExpressionNode;
	private ArrayList<TokenAnalyzer> _tokenAnalyzers;
	private ArrayList<FunctionAnalyzer> _functionAnalyzers;
	private ArrayList<BinaryOpAnalyzer> _binaryOpAnalyzers;
	private boolean _enforceNodeType;
    private NodeType _enforcedNodeType;
    
	/**
	 * Builds a Tokenizer that parses a given expression.
	 * It's initialized with a set of default tokens to search for.
	 * @param expression The expression to parse.
	 */
	public Tokenizer()
	{
		_tokenAnalyzers = new ArrayList<TokenAnalyzer>();
		_functionAnalyzers = new ArrayList<FunctionAnalyzer>();
		_binaryOpAnalyzers = new ArrayList<BinaryOpAnalyzer>();
        _enforceNodeType = false;
        
        addDefaultAnalyzers();
	}
	
	/**
	 * Add an analyzer to the list of token analyzers that will be used to parse the expression.
	 * @param analyzer The analyzer to add.
	 */
	public void addAnalyzer(TokenAnalyzer analyzer)
	{
		_tokenAnalyzers.add(analyzer);
	}
	
	/**
	 * Add an function analyzer to the list of token analyzers that will be used to search for functions.
	 * @param analyzer The function analyzer to add.
	 */
	public void addFunctionAnalyzer(FunctionAnalyzer analyzer)
	{
		_functionAnalyzers.add(analyzer);
	}
	
	/**
	 * Add an binary operator analyzer to the list of token analyzers that will be used to search for binary operators.
	 * @param analyzer The binary analyzer to add.
	 */
	public void addBinaryOpAnalyzer(BinaryOpAnalyzer analyzer)
	{
		_binaryOpAnalyzers.add(analyzer);
	}
	
	/**
	 * Checks if it has a token analyzer that matches the beginning of the current expression.
	 * If so, then the current expression is updated and the correct ExpressionNode is generated.
	 * Otherwise, a TokenNotRecognized exception is thrown.
	 * @throws TokenNotRecognized if the current expression starts with an unrecognized token.
	 */
	public void advance() throws TokenNotRecognized
	{
		int endPos;
		int maxMatched = -1;
		ExpressionNode closestNode = null;
		
		ArrayList<TokenAnalyzer> analyzers = new ArrayList<TokenAnalyzer>();
		
		analyzers.addAll(_tokenAnalyzers);
		analyzers.addAll(_binaryOpAnalyzers);
		analyzers.addAll(_functionAnalyzers);
		
		for (TokenAnalyzer analyzer : analyzers)
		{
			endPos = analyzer.getEndOfPatternPosition(_expression);
			if (endPos >= 0)
			{
				ExpressionNode currentNode = analyzer.getExpressionNode();

                //if we're looking for a specific node type then skip over the wrong ones
                if ( (_enforceNodeType) && !(currentNode.getType().equals(_enforcedNodeType)))
                    continue;
                
                if (endPos > maxMatched)
                {
                	maxMatched = endPos;
                	closestNode = currentNode;
                }
                
			}
		}
		
		if (maxMatched >= 0)
		{
			_currentExpressionNode = closestNode;
			_expression = _expression.substring(maxMatched);
			return;
		}
		
		throw new TokenNotRecognized("An unexpected token was encountered in " + _expression);
	}

    /**
	 * Same as advance but enforces a specific node type.
	 * @throws TokenNotRecognized if the current expression starts with an unrecognized token.
	 */
	public void advance(NodeType enforcedType) throws TokenNotRecognized
	{
		_enforceNodeType = true;
        _enforcedNodeType = enforcedType;
        advance();
        _enforceNodeType = false;
    }
    
	/**
	 * Get the expression node corresponding to the beginning of the expression.
	 * @return The expression node.
	 */
	public ExpressionNode getExpressionNode()
	{
		return _currentExpressionNode;
	}
	
	/**
	 * Checks if the expression is empty
	 * @return True if the expression is empty.
	 */
	public boolean isEmpty()
	{
		return _expression.length() == 0;
	}
	
	/**
	 * Set the expression that the tokenizer should parse.
	 * @param expression
	 */
	public void setExpression(String expression)
	{
		_expression = expression;
	}
	
	/**
	 * Get an expression node corresponding to a certain function name.
	 * @param functionName The name of the function we want.
	 * @return The required expression node.
	 * @throws TokenNotRecognized if the function name wasn't found.
	 */
	public FunctionNode getFunctionNode(String functionName) throws TokenNotRecognized
	{
		FunctionNode output;
		
		for (FunctionAnalyzer analyzer : _functionAnalyzers)
		{
			if (analyzer.getFunctionName().equals(functionName))
			{
				output = (FunctionNode) analyzer.getExpressionNode();
				return output;
			}
		}			
		
		throw new TokenNotRecognized("An unexpected function name was encountered");
	}

	/**
	 * Remove a function from the list of functions to search for.
	 * @param functionName The function to remove.
	 */
    public void removeFunctionAnalyzer(String functionName)
	{
		ArrayList<FunctionAnalyzer> newFunctionAnalyzers = new ArrayList<FunctionAnalyzer>();

		for (FunctionAnalyzer analyzer : _functionAnalyzers)
		{
			if (!analyzer.getFunctionName().equals(functionName))
			{
				newFunctionAnalyzers.add(analyzer);
			}
		}

		_functionAnalyzers = newFunctionAnalyzers;
	}
	
    /**
	 * Remove a binary operator from the list of functions to search for.
	 * @param op The function to remove.
	 */
	public void removeBinaryOp(String op)
	{
		op = " " + op + " ";
		ArrayList<BinaryOpAnalyzer> newBinaryOpAnalyzers = new ArrayList<BinaryOpAnalyzer>();
		int endPos;
		
		for (BinaryOpAnalyzer analyzer : _binaryOpAnalyzers)
		{
			endPos = analyzer.getEndOfPatternPosition(op);
			if (endPos < op.length())
				newBinaryOpAnalyzers.add(analyzer);
		}
		
		_binaryOpAnalyzers = newBinaryOpAnalyzers;	
	}
	
	private void addDefaultAnalyzers()
	{
		addBinaryOpAnalyzer(new AddOpAnalyzer());
		addBinaryOpAnalyzer(new SubOpAnalyzer());
		addBinaryOpAnalyzer(new MulOpAnalyzer());
		addBinaryOpAnalyzer(new DivOpAnalyzer());
        
        addAnalyzer(new NegOpAnalyzer());
        addAnalyzer(new ConstantAnalyzer());
		addAnalyzer(new OpenParenthesisAnalyzer());
		addAnalyzer(new SymbolAnalyzer());
		
        addFunctionAnalyzer(new MaxAnalyzer());
        addFunctionAnalyzer(new MinAnalyzer());
        addFunctionAnalyzer(new ModAnalyzer());
        addFunctionAnalyzer(new PowAnalyzer());
        addFunctionAnalyzer(new AbsAnalyzer());
        addFunctionAnalyzer(new ExpAnalyzer());
        addFunctionAnalyzer(new LogAnalyzer());
        
		
	}
	
}
